<template>
  <view class="components-input-box" :style="[cusStyle]" :class="[cusClass]">
    <view
      class="icon-button minus-icon-button"
      :class="Number(data.innerValue) <= Number(min) || disabled || data.disabled ? 'disabled' : ''"
      :hoverStartTime="0"
      :hoverStayTime="0"
      hoverClass="hover-class"
      :style="minusButtonStyle"
      @click.stop.prevent="minusClick"
      @touchstart="onTouchStart('minus')"
      @touchend.stop="clearLongTimeout"
    >
      <mxp-icon
        :name="data.minusLoading || data.loading ? 'jiazai' : 'jianhao'"
        :spin="data.minusLoading || data.loading"
        :size="minusIconSize"
        :color="minusIconColor"
        :cusStyle="minusIconStyle"
      ></mxp-icon>
    </view>
    <input
      v-model="data.innerValue"
      :style="[inputStyle]"
      class="input"
      :type="data.type"
      :inputmode="data.inputmode"
      :focus="focus"
      :confirmType="confirmType"
      :placeholder="newPlaceholder"
      :placeholderStyle="newPlaceholderStyle"
      :placeholderClass="placeholderClass"
      :cursorSpacing="cursorSpacing"
      :adjustPosition="adjustPosition"
      :selectionStart="selectionStart"
      :selectionEnd="selectionEnd"
      :confirmHold="confirmHold"
      :controlled="controlled"
      :alwaysSystem="alwaysSystem"
      :disabled="inputDisabled || disabled || data.disabled"
      :autoBlur="autoBlur"
      :alwaysEmbed="alwaysEmbed"
      :holdKeyboard="holdKeyboard"
      :ignoreCompositionEvent="ignoreCompositionEvent"
      @blur="onBlurEvent"
      @focus="onFocusEvent"
      @input="onInputEvent"
      @confirm="onConfirm"
      @keyboardheightchange="onKeyboardheightchange"
    />
    <view
      class="icon-button plus-icon-button"
      :class="Number(data.innerValue) >= Number(max) || disabled || data.disabled ? 'disabled' : ''"
      :hoverStartTime="0"
      :hoverStayTime="0"
      hoverClass="hover-class"
      :style="plusButtonStyle"
      @click.stop.prevent="plusClick"
      @touchstart="onTouchStart('plus')"
      @touchend.stop="clearLongTimeout"
    >
      <mxp-icon
        :name="data.plusLoading || data.loading ? 'jiazai' : 'jianhao'"
        :spin="data.plusLoading || data.loading"
        :size="plusIconSize"
        :color="plusIconColor"
        :cusStyle="plusIconStyle"
      ></mxp-icon>
    </view>
  </view>
</template>

<script lang="ts" setup>
  import { type CSSProperties, type PropType, computed, inject, nextTick, onBeforeMount, onMounted, onUnmounted, shallowReactive, watch } from 'vue';
  import { publicStyeProps } from '../mxp-tool/utils/public-props';
  import { type FormID, formInjectKey, formItemPropInjectKey, mxpFormStore } from '../mxp-tool/store/mxp-store-form';
  import { round } from '@/mxp-ui/mxp-tool/utils/number';
  import type { $AllColorStr, $InputVerify, $SizeUnitStr } from '@/mxp-ui/mxp-tool/types/interface';
  import { cssObj2Str } from '../mxp-tool/utils/dom';
  import MxpIcon from '../mxp-icon/mxp-icon.vue';
  import { verifyNumbInput } from '@/mxp-ui/mxp-tool/utils/verify-number';
  import type {
    InputConfirmType,
    InputInputMode,
    InputOnBlurEvent,
    InputOnConfirmEvent,
    InputOnFocusEvent,
    InputOnInputEvent,
    InputOnKeyboardheightchangeEvent,
    InputType,
  } from 'mxp-uni-types';

  defineOptions({
    name: 'mxp-input-number-box',
  });

  const props = defineProps({
    /**
     * 支持验证数字的类型
     * @param add_int - 正整数 (不含0) 主要用于输入时验证
     * @param minus_int - 负整数(不含0) 主要用于输入时验证
     * @param all_int - 正负整数(不含0) 主要用于输入时验证
     * @param add_int_or_zero - 正整数 (含0) 主要用于输入时验证
     * @param minus_int_or_zero - 负整数 (含0) 主要用于输入时验证
     * @param all_int_or_zero - 正负整数(含0) 主要用于输入时验证
     * @param add_float - 正浮点数  主要用于输入时验证
     * @param minus_float - 负浮点数 主要用于输入时验证
     * @param all_float - 正负浮点数   主要用于输入时验证
     */
    verify: { type: String as PropType<$InputVerify>, default: 'all_float' },
    /** 是否强制返回为字符串类型 */
    returnString: { type: Boolean, default: false },
    /** 是否自动聚焦 */
    focus: { type: Boolean, default: false },
    /** 是否禁用全部禁用(按钮和输入框) */
    disabled: { type: Boolean, default: false },
    /** 是否禁用输入框 */
    inputDisabled: { type: Boolean, default: undefined },
    /** 步长 */
    step: { type: Number, default: 1 },
    /** 保留多少位小数 */
    decimals: { type: Number, default: 2 },
    /** 绑定的值 */
    modelValue: { type: [String, Number], default: '' },
    /** 是否允许长按 按钮 添加或减少 */
    longPress: { type: Boolean, default: true },
    /** 最小值 */
    min: { type: Number, default: undefined },
    /** 最大值 */
    max: { type: Number, default: undefined },
    /** 设置右下角按钮的文字，兼容性详见uni-app文档 */
    confirmType: { type: String as PropType<InputConfirmType>, default: 'go' },
    /** 输入框为空时的占位符 */
    placeholder: { type: [String, Number], default: '' },
    /** 指定placeholder的样式类，注意页面或组件的style中写了scoped时，需要在类名前写/deep/部分平台可能还需要写到 app.vue中才生效 */
    placeholderClass: { type: String, default: 'textarea-placeholder' },
    /** 指定placeholder的样式 */
    placeholderStyle: { type: Object as PropType<CSSProperties>, default: () => ({ color: '#c0c4cc' }) },
    /** 输入框的样式 */
    inputStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
    /** 增加图标颜色 */
    plusIconStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
    /**减少图标大小*/
    minusIconStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
    /**减少图标大小*/
    minusButtonStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
    /** 增加图标大小 */
    plusButtonStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
    /** 指定光标与键盘的距离，取 input 距离底部的距离和 cursor-spacing 指定的距离的最小值作为光标与键盘的距离 */
    cursorSpacing: { type: Number, default: 8 },
    /** 键盘弹起时，是否自动上推页面, App-Android(vue 页面 softinputMode 为 adjustResize 时无效，使用 x5 内核时无效)、微信小程序、百度小程序、QQ小程序、京东小程序 */
    adjustPosition: { type: Boolean, default: true },
    /** 点击键盘右下角按钮时是否保持键盘不收起 */
    confirmHold: { type: Boolean, default: false },
    /** 光标起始位置，自动聚集时有效，需与selection-end搭配使用 */
    selectionStart: { type: Number, default: undefined },
    /** 光标结束位置，自动聚集时有效，需与selection-start搭配使用 */
    selectionEnd: { type: Number, default: undefined },
    /** 是否为受控组件。为 true 时，value 内容会完全受 setData 控制 (支付宝小程序 1.9.0+) */
    controlled: { type: Boolean, default: false },
    /** 键盘收起时，是否自动失去焦点 (App-Vue 3.0.0+) */
    autoBlur: { type: Boolean, default: false },
    /** 是否忽略组件内对文本合成系统事件的处理。为 false 时将触发 compositionstart、compositionend、compositionupdate 事件，且在文本合成期间会触发 input 事件 (App-vue (3.4.4+)、H5 (3.4.4+)、App-nvue不支持) */
    ignoreCompositionEvent: { type: Boolean, default: true },
    /** 是否忽略组件内对文本合成系统事件的处理。为 false 时将触发 compositionstart、compositionend、compositionupdate 事件，且在文本合成期间会触发 input 事件 (App-vue (3.4.4+)、H5 (3.4.4+)、App-nvue不支持) */
    holdKeyboard: { type: Boolean, default: false },
    /** 是否强制使用系统键盘和 Web-view 创建的 input 元素。为 true 时，confirm-type、confirm-hold 可能失效  (支付宝小程序 2.7.3+) */
    alwaysSystem: { type: Boolean, default: false },
    /** 强制 input 处于同层状态，默认 focus 时 input 会切到非同层状态 (微信小程序 2.10.4+) */
    alwaysEmbed: { type: Boolean, default: false },
    /** 开关状态改变前执行的函数(需要返回 true | false 表示是否改变 ) */
    beforeChange: { type: [Function, undefined] as PropType<((numb: number | string) => Promise<boolean> | boolean) | undefined>, default: undefined },
    ...publicStyeProps,
  });

  const emits = defineEmits<{
    (e: 'update:modelValue', str: number | string): void;
    (e: 'input', event: InputOnInputEvent): void;
    (e: 'blur', event: InputOnBlurEvent): void;
    (e: 'focus', event: InputOnFocusEvent): void;
    (e: 'keyboardheightchange', event: InputOnKeyboardheightchangeEvent): void;
    (e: 'confirm', event: InputOnConfirmEvent): void;
    (e: 'change', str: number | string): void;
  }>();

  const plusIconSize = computed(() => (props.plusIconStyle.fontSize || props.plusIconStyle['font-size'] || '16px') as $SizeUnitStr);
  const plusIconColor = computed(() => (props.plusIconStyle.color || '#ffffff') as $AllColorStr);
  const minusIconSize = computed(() => (props.plusIconStyle.fontSize || props.plusIconStyle['font-size'] || '16px') as $SizeUnitStr);
  const minusIconColor = computed(() => (props.minusIconStyle.color || '#ffffff') as $AllColorStr);

  const data = shallowReactive({
    disabled: false,
    type: 'text' as InputType,
    inputmode: 'url' as InputInputMode,
    loading: false,
    minusLoading: false,
    plusLoading: false,
    innerValue: props.modelValue,
    longPressTimer: undefined as NodeJS.Timeout | undefined,
    okValue: '' as number | string, // 保存上次的正确结果
  });

  watch(
    () => props.modelValue,
    (newVal) => {
      if (data.innerValue !== newVal) {
        updateNumb(newVal, 'change');
      }
    }
  );

  let mxpFromIDInjectKey: FormID | undefined = undefined;
  let propInjectKey: number | string | undefined = undefined;
  onMounted(() => {
    mxpFromIDInjectKey = inject<FormID>(formInjectKey);
    propInjectKey = inject<number | string>(formItemPropInjectKey);
  });

  onUnmounted(() => {
    clearLongTimeout();
  });

  /** 最终的占位字符串 */
  const newPlaceholder = computed(() => String(props.placeholder || ''));

  /** 最终的占位文字样式 */
  const newPlaceholderStyle = computed(() => cssObj2Str(props.placeholderStyle));

  onBeforeMount(() => {
    const isPlus = ['add_int', 'add_int_or_zero', 'add_float'].includes(props.verify || '');
    // #ifdef MP || APP-NVUE
    data.type = isPlus ? 'digit' : 'text';
    // #endif

    // #ifdef APP-PLUS
    data.type = isPlus ? 'number' : 'text';
    // #endif

    data.inputmode = ['add_int', 'add_int_or_zero'].includes(props.verify || '') ? 'numeric' : props.verify === 'add_float' ? 'decimal' : 'url';
    updateNumb(props.modelValue, 'change');
  });

  /** 点击键盘完成按钮触发 */
  function onConfirm(event: InputOnConfirmEvent) {
    try {
      uni.hideKeyboard();
    } catch (e) {}
    nextTick(() => {
      emits('confirm', event);
    });
  }

  /** 改变时触发 */
  function onInputEvent(event: InputOnInputEvent) {
    updateNumb(event.detail.value, 'input');
    nextTick(() => {
      setTimeout(() => {
        emits('change', data.innerValue);
        emits('input', event);
        // 尝试调用mxp-form组件的验证
        const validateField = mxpFromIDInjectKey ? mxpFormStore().state[mxpFromIDInjectKey]?.validateField : undefined;
        typeof validateField === 'function' && (propInjectKey || propInjectKey === 0) && validateField(propInjectKey, 'change');
      }, 10);
    });
  }

  /** 获得焦点 */
  function onFocusEvent(event: InputOnFocusEvent) {
    emits('focus', event);
  }

  /** 失去焦点 */
  function onBlurEvent(event: InputOnBlurEvent) {
    if (event.detail.value.toString() !== data.innerValue.toString()) {
      // 解决部分机型在英文联想输入下划线时oninput获取不到问题
      updateNumb(event.detail.value, 'blur');
    }
    emits('blur', event);
    // 尝试调用mxp-form组件的验证
    const validateField = mxpFromIDInjectKey ? mxpFormStore().state[mxpFromIDInjectKey]?.validateField : undefined;
    typeof validateField === 'function' && (propInjectKey || propInjectKey === 0) && validateField(propInjectKey, 'blur');
  }

  /**
   * 更新数字
   */
  async function updateNumb(value: number | string, type: 'blur' | 'change' | 'input' | 'minus' | 'plus') {
    let newVal = value !== '' ? getFinalValue(value) : '';
    const oldNumb = props.modelValue;
    if (String(newVal) !== String(data.innerValue) || String(newVal) !== String(props.modelValue)) {
      if (typeof props.beforeChange === 'function') {
        try {
          data.disabled = true;
          if (type === 'minus') {
            data.minusLoading = true;
          } else if (type === 'plus') {
            data.plusLoading = true;
          } else {
            data.loading = true;
          }
          const res = await props.beforeChange(newVal);
          data.disabled = false;
          data.minusLoading = false;
          data.plusLoading = false;
          data.loading = false;
          // eslint-disable-next-line require-atomic-updates
          newVal = res ? newVal : oldNumb;
        } catch (e) {
          data.disabled = false;
          data.minusLoading = false;
          data.plusLoading = false;
          data.loading = false;
          // eslint-disable-next-line require-atomic-updates
          newVal = oldNumb;
        }
      }

      nextTick(() => {
        if (props.returnString) {
          data.innerValue = String(newVal);
        } else {
          data.innerValue = isNaN(Number(newVal)) || ['', '-', '.'].includes(String(newVal)) || String(newVal).endsWith('.') ? newVal : Number(newVal);
          data.okValue = data.innerValue;
        }
        emits('update:modelValue', data.innerValue);
      });
    }
  }

  /**
   * 得到验证后的最终值
   */
  function getFinalValue(value: number | string) {
    let innerValue = value;
    if (innerValue !== '' && innerValue !== '-') {
      innerValue = verifyNumbInput(props.verify, innerValue, data.okValue);
      if (!isNaN(Number(innerValue)) && !String(innerValue).endsWith('.')) {
        innerValue = verifyNumbSize(innerValue, props.max, props.min);
        const srtVal = String(innerValue);
        if (typeof props.decimals === 'number' && props.decimals >= 0 && srtVal.indexOf('.') > 0 && String(Number(innerValue)) === srtVal) {
          innerValue = round(Number(innerValue), props.decimals);
        }
      }
    }
    return String(Number(innerValue)) === String(innerValue) ? Number(innerValue) : innerValue;
  }

  /**
   * 处理最大最小值 (当超过最大最小值时用最大最小值代替)
   */
  function verifyNumbSize(numb: number | string, maxNumb?: number, minNumb?: number) {
    const newNumb = Number(numb);
    if (!String(newNumb).endsWith('.') && !isNaN(newNumb)) {
      if (typeof maxNumb === 'number' && newNumb > maxNumb) {
        return maxNumb;
      } else if (typeof minNumb === 'number' && newNumb < minNumb) {
        return minNumb;
      }
    }
    // 注意一定要返回numb 而不是 newNumb，j解决 0.01 问题
    return numb;
  }

  /** 键盘高度发生变化的时候触发此事件，event.detail = {height: height, duration: duration} */
  function onKeyboardheightchange(event: InputOnKeyboardheightchangeEvent) {
    emits('keyboardheightchange', event);
  }
  /** 添加 */
  function plusClick() {
    if (!props.disabled || !data.disabled) {
      if (isNaN(Number(data.innerValue))) {
        updateNumb(props.step, 'plus');
      } else {
        const nubm = Number(data.innerValue) + props.step;
        updateNumb(nubm, 'plus');
      }
    }
  }

  /** 减少 */
  function minusClick() {
    if (!props.disabled || !data.disabled) {
      if (isNaN(Number(data.innerValue))) {
        updateNumb(props.step, 'minus');
      } else {
        const nubm = Number(data.innerValue) - props.step;
        updateNumb(nubm, 'minus');
      }
    }
  }
  /** 清除定时器 */
  function clearLongTimeout() {
    clearTimeout(data.longPressTimer);
    data.longPressTimer = undefined;
  }

  /** 监听触摸开始事件 */
  function onTouchStart(type: 'minus' | 'plus') {
    if (props.longPress) {
      clearLongTimeout();
      data.longPressTimer = setTimeout(() => {
        longPressStep(type);
      }, 600);
    }
  }
  /** 长按 */
  function longPressStep(type: 'minus' | 'plus') {
    // 每隔一段时间，重新调用longPressStep方法，实现长按加减
    clearLongTimeout();
    if (!props.disabled) {
      data.longPressTimer = setTimeout(() => {
        type === 'minus' && minusClick();
        type === 'plus' && plusClick();
        longPressStep(type);
      }, 180);
    }
  }
</script>

<style lang="scss" scoped>
  .components-input-box {
    display: flex;
    flex-flow: row nowrap;
    align-items: center;
    justify-content: flex-start;

    .icon-button {
      display: flex;
      flex-flow: row nowrap;
      align-items: center;
      justify-content: center;
      width: 28px;
      height: 28px;
      background-color: #223aec;
    }

    .input {
      width: 50px;
      height: 28px;
      padding: 0 10px;
      font-size: 14px;
      line-height: 14px;
      text-align: center;
      text-shadow: none;
      user-select: text;
      background-color: #ffffff;
      border: none;
      border-style: none;
      outline: none;
      outline-color: transparent;
      box-shadow: none;
      appearance: none;
    }

    .minus-icon-button {
      border-radius: 4px 0 0 4px;
    }

    .plus-icon-button {
      border-radius: 0 4px 4px 0;
    }
  }

  .hover-class {
    opacity: 0.7;
  }

  .disabled {
    opacity: 0.7;
  }
</style>
